﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.VisualStudio.Text;
using Red.Scripts.Defines.Utilities;

namespace Language.Stub
{
    #region Interfaces

    public interface IScope
    {
        Scope Scope { get; }
    }

    public class Symbol
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string File { get; set; }
        public int Line { get; set; }
    }

    [Flags]
    public enum SearchFlags
    {
        None = 0,
        IncludeParameters = 1 << 0,
        IncludeLocals = 1 << 1,
        IncludeEnumOptions = 1 << 2,
    }

    public interface IFlattener
    {
        IEnumerable<Symbol> Flatten( SearchFlags searchFlags = SearchFlags.None );
        IEnumerable<Symbol> Flatten( string typename, string parentPrefix, SearchFlags searchFlags );
    }

    #endregion Interfaces

    #region Stubs

    public partial class Configuration
    {
        #region Public Types

        [Flags]
        public enum Flag
        {
            // Applies to Members
            AccessPrivate = 1 << 0,
            AccessProtected = 1 << 1,

            // Applies to Types, Members
            Imported = 1 << 2,

            // Applies to Properties
            ScopeLocal = 1 << 3,
            ScopeParameter = 1 << 4,
            ScopeMember = 1 << 5,

            // Applies to Functions
            Operator = 1 << 6,
            Static = 1 << 7,
        }

        #endregion Public Types

        #region Public Properties

        public Flag Flags { get; set; }

        public bool IsPrivate => Flags.HasFlag( Flag.AccessPrivate );
        public bool IsProtected => Flags.HasFlag( Flag.AccessProtected );
        public bool IsPublic => !IsPrivate && !IsProtected;

        public bool IsNative => Flags.HasFlag( Flag.Imported );

        public bool IsLocal => Flags.HasFlag( Flag.ScopeLocal );
        public bool IsParameter => Flags.HasFlag( Flag.ScopeParameter );
        public bool IsMember => Flags.HasFlag( Flag.ScopeMember );
        public bool IsGlobal => !IsLocal && !IsParameter && !IsMember;

        public bool IsOperator => Flags.HasFlag( Flag.Operator );
        public bool IsStatic => Flags.HasFlag( Flag.Static );

        #endregion Public Properties
    }

    public partial class PositionContext
    {
        #region Public Properties

        public int Start { get; set; }
        public int End { get; set; }

        #endregion Public Properties
    }

    public partial class Context
    {
        #region Public Properties

        public string File { get; set; }
        public int Line { get; set; }

        #endregion Public Properties
    }

    public partial class DetailedContext : Context
    {
        #region Public Properties

        public PositionContext Position { get; set; } = new PositionContext();
        public int LineStart { get; set; }

        #endregion Public Properties
    }

    public partial class NameContext : DetailedContext
    {
        #region Public Properties

        public string Name { get; set; }

        #endregion Public Properties
    }

    public partial class ConfiguredContext : NameContext
    {
        #region Public Properties

        public Configuration Configuration { get; private set; } = new Configuration();

        #endregion Public Properties
    }

    public partial class Scope
    {
        public DetailedContext Start { get; set; } = new DetailedContext();
        public DetailedContext End { get; set; } = new DetailedContext();

        public bool Contains( int position )
        {
            return Start.Position.Start <= position && End.Position.End > position;
        }
    }

    public partial class Option : NameContext
    {
        public Enum Parent { get; set; }
        public long Value { get; set; }
    }

    public partial class Enum : ConfiguredContext, IFlattener, IScope
    {
        public List<Option> Options { get; private set; } = new List<Option>();

        #region IScope

        public Scope Scope { get; private set; } = new Scope();

        #endregion IScope

        #region IFlattener

        public IEnumerable<Symbol> Flatten( SearchFlags searchFlags )
        {
            return Flatten( "Enum", string.Empty, searchFlags );
        }

        public IEnumerable<Symbol> Flatten( string typename, string parentPrefix, SearchFlags searchFlags )
        {
            string name = $"{parentPrefix}{Name}";

            IEnumerable<Symbol> symbols = new List<Symbol>()
            {
                new Symbol()
                {
                    Name = name,
                    Type = typename,
                    Line = Line,
                    File = File
                }
            };

            if ( searchFlags.HasFlag( SearchFlags.IncludeEnumOptions ) )
            {
                symbols = symbols.Concat
                (
                    from option in Options
                    select new Symbol()
                    {
                        Name = $"{name}.{option.Name}",
                        Type = "Enum option",
                        Line = option.Line,
                        File = option.File
                    }
                );
            }

            return symbols;
        }

        #endregion IFlattener
    }

    public partial class TypedContext : ConfiguredContext
    {
        public NameContext TypeDeclaration { get; set; } = new NameContext();
        public NameContext TypeDefinition { get; set; } = new NameContext();
    }

    public partial class Property : TypedContext
    {
        public class EqualityComparer : IEqualityComparer<Stub.Property>
        {
            public bool Equals( Property x, Property y )
            {
                if ( x.Name != y.Name )
                    return false;

                return true;
            }

            public int GetHashCode( Property obj )
            {
                return obj.GetHashCode();
            }
        }
    }

    public partial class Class : TypedContext, IFlattener, IScope
    {
        public List<Property> Properties { get; private set; } = new List<Property>();
        public List<Function> Functions { get; private set; } = new List<Function>();
        
        public IEnumerable<Function> GetFunctionOverloads( string name )
        {
            return from function in Functions where function.Name == name select function;
        }
        
        public IEnumerable<Function> GetFunctionShortcuts( IEnumerable<Function> functions )
        {
            return from function in functions
                   where function.Configuration.IsStatic && function.Parameters.FirstOrDefault()?.TypeDeclaration.Name == Name
                   select Function.CreateStaticTransformed( function );
        }

        #region IScope

        public Scope Scope { get; private set; } = new Scope();

        #endregion IScope

        #region IFlattener

        public IEnumerable<Symbol> Flatten( SearchFlags searchFlags )
        {
            return Flatten( "Class", string.Empty, searchFlags );
        }

        public IEnumerable<Symbol> Flatten( string typename, string parentPrefix, SearchFlags searchFlags )
        {
            string name = $"{parentPrefix}{Name}";

            return new List<Symbol>()
            {
                new Symbol()
                {
                    Name = name,
                    Type = typename,
                    Line = Line,
                    File = File
                }
            }.Concat
            (
                from property in Properties
                select new Symbol()
                {
                    Name = $"{name}.{property.Name}",
                    Type = $"{typename} property",
                    Line = property.Line,
                    File = property.File
                }
            ).Concat
            (
                Functions.SelectMany( function => function.Flatten( $"{typename} method", $"{name}.", searchFlags ) )
            );
        }

        #endregion IFlattener
    }

    public partial class Function : TypedContext, IFlattener, IScope
    {
        public Class Parent { get; private set; }
        public List<Property> Parameters { get; private set; } = new List<Property>();
        public List<Property> Locals { get; private set; } = new List<Property>();

        public IEnumerable<Function> GetOverloads()
        {
            if ( Parent != null )
            {
                var overloads = Parent.GetFunctionOverloads( Name );

                return overloads.Concat( Parent.GetFunctionShortcuts( overloads ) );
            }

            return new Function[] { this };
        }

        public bool Is( Function other )
        {
            if ( other.Name != Name )
                return false;

            if ( other.Parent?.Name != Parent?.Name )
                return false;

            if ( !Enumerable.SequenceEqual( Parameters, other.Parameters, new Property.EqualityComparer() ) )
                return false;

            return true;
        }

        public bool IsOverloadOf( Function other )
        {
            return other.Name == Name && other.Parent?.Name == Parent?.Name;
        }

        #region IScope

        public Scope Scope { get; private set; } = new Scope();

        #endregion IScope

        #region IFlattener

        public IEnumerable<Symbol> Flatten( SearchFlags searchFlags )
        {
            return Flatten( "Function", string.Empty, searchFlags );
        }

        public IEnumerable<Symbol> Flatten( string typename, string parentPrefix, SearchFlags searchFlags )
        {
            string name = $"{parentPrefix}{Name}";

            IEnumerable<Symbol> symbols = new List<Symbol>()
            {
                new Symbol()
                {
                    Name = name,
                    Type = typename,
                    Line = Line,
                    File = File
                }
            };

            if ( searchFlags.HasFlag( SearchFlags.IncludeParameters ) )
            {
                symbols = symbols.Concat
                (
                    from parameter in Parameters
                    select new Symbol()
                    {
                        Name = $"{name}.{parameter.Name}",
                        Type = $"{typename} parameter",
                        Line = parameter.Line,
                        File = parameter.File
                    }
                );
            }

            if ( searchFlags.HasFlag( SearchFlags.IncludeLocals ) )
            {
                symbols = symbols.Concat
                (
                    from local in Locals
                    select new Symbol()
                    {
                        Name = $"{name}.{local.Name}",
                        Type = $"{typename} local",
                        Line = local.Line,
                        File = local.File
                    }
                );
            }

            return symbols;
        }

        #endregion IFlattener
    }

    public partial class Comment : DetailedContext
    {
    }

    public enum LiteralType
    {
        Int,
        Float,
        String,
        Name,
        Keyword,
        Integral
    }

    public partial class Literal : DetailedContext
    {
        public LiteralType Type { get; set; }
    }

    public partial class Token : DetailedContext
    {
        public DetailedContext Declaration { get; set; }
    }

    public partial class ScopedToken : Token
    {
        public PositionContext Scope { get; private set; } = new PositionContext();
    }

    public partial class File
    {
        #region Public Properties

        public string Path { get; private set; }
        public List<IFlattener> FlatStubs { get; private set; } = new List<IFlattener>();

        public SortedList<int, DetailedContext> Stubs { get; private set; } = new SortedList<int, DetailedContext>();
        public SortedList<int, Token> Tokens { get; private set; } = new SortedList<int, Token>();
        public SortedList<int, Token> SupplementaryTokens { get; private set; } = new SortedList<int, Token>();
        public SortedList<int, Literal> Literals { get; private set; } = new SortedList<int, Literal>();

        public List<NameContext> NamedStubs { get; private set; } = new List<NameContext>();
        public SortedList<int, IScope> Scopes { get; private set; } = new SortedList<int, IScope>();
        public SortedList<int, ScopedToken> ScopedTokens { get; private set; } = new SortedList<int, ScopedToken>();
        public SortedList<int, DetailedContext> Breakpoints { get; private set; } = new SortedList<int, DetailedContext>();


        /// <summary>
        /// The snapshot from which this file was generated
        /// </summary>
        public ITextSnapshot Snapshot { get; private set; }

        #endregion Public Properties

        public IEnumerable< IScope > FindScopes( int position )
        {
             int index = Scopes.LowerBound( position );

            if ( index < 0 || index >= Scopes.Count )
                yield break;

            IScope scope;
            while ( index < Scopes.Count && ( scope = Scopes.Values[ index ] ) != null && scope.Scope.End.Position.End >= position )
            {
                if( scope.Scope.Start.Position.Start <= position )
                {
                    yield return scope;
                }

                ++index;
            }
        }

        public Token FindToken( int position, bool exact )
        {
            Token token = FindToken( Tokens, position, exact );

            if ( token == null )
                token = FindToken( SupplementaryTokens, position, exact );

            if ( token == null )
                token = FindToken( ScopedTokens, position, exact );

            return token;
        }

        private static Token FindToken( SortedList<int, Token> tokens, int position, bool exact )
        {
            int index = tokens.LowerBound( position );

            if ( index >= 0 && index < tokens.Count )
            {
                Token token = tokens.Values[ index ];

                if ( exact )
                {
                    if ( token.Position.End == position )
                    {
                        return token;
                    }
                }
                else
                {
                    if ( token.Position.Start <= position && token.Position.End >= position )
                    {
                        return token;
                    }
                }
            }

            return null;
        }

        private static Token FindToken( SortedList<int, ScopedToken> tokens, int position, bool exact )
        {
            int index = tokens.LowerBound( position );

            if ( index >= 0 && index < tokens.Count )
            {
                ScopedToken token = tokens.Values[ index ];

                if ( exact )
                {
                    if ( token.Scope.End == position )
                    {
                        return token;
                    }
                }
                else
                {
                    if ( token.Scope.End <= position && token.Position.Start >= position )
                    {
                        return token;
                    }
                }
            }

            return null;
        }
    }

    #endregion Stubs

    #region Database

    [Export]
    public sealed partial class Database
    {
        #region Public Properties

        public Dictionary<string, File> FileByPath { get; private set; } = new Dictionary<string, File>();

        #endregion Public Properties

        #region Public Methods

        /// <summary>
        /// Find a file from it's absolute path
        /// </summary>
        /// <param name="path">Absolute path</param>
        /// <returns>Stub.File object</returns>
        public File FindFile( string path )
        {
            Utility.FilePath processedPath = new Utility.FilePath() { Absolute = path };

            File file;
            FileByPath.TryGetValue( processedPath.Relative, out file );

            return file;
        }

        /// <summary>
        /// Take an incomplete context (such as a declaration context) and find the associated complete context (such as a class stub)
        /// </summary>
        /// <remarks>
        /// If a complete context is passed in, then the same context should also be returned
        /// </remarks>
        public DetailedContext ResolveContext( DetailedContext source )
        {
            if ( source == null )
                return null;

            File file;
            if ( !FileByPath.TryGetValue( source.File, out file ) )
                return null;

            DetailedContext target = null;
            file.Stubs.TryGetValue( source.Position.End, out target );

            return target;
        }

        public DetailedContext ResolveDeclaration( TypedContext context )
        {
            return ResolveContext( context?.TypeDeclaration );
        }

        public DetailedContext ResolveDefinition( TypedContext context )
        {
            return ResolveContext( context?.TypeDefinition );
        }

        /// <summary>
        /// Find a token
        /// </summary>
        /// <param name="path">Absolute path to file</param>
        /// <param name="position">Location in buffer to search for a token</param>
        /// <param name="exact"><paramref name="position"/> exactly matches the end point of the token you're looking for</param>
        /// <returns><see cref="Token"/> if successful, null if nothing was found</returns>
        public Token FindToken( string path, int position, bool exact )
        {
            return FindFile( path )?.FindToken( position, exact );
        }

        public DetailedContext FindDeclaration( string path, int position )
        {
            Utility.FilePath processedPath = new Utility.FilePath() { Absolute = path };

            string relativePath = processedPath.Relative;

            if ( string.IsNullOrWhiteSpace( relativePath ) )
                return null;

            File file;
            if ( !FileByPath.TryGetValue( relativePath, out file ) )
                return null;

            DetailedContext found;

            if ( ( found = Search( file.Tokens, position ) ) != null )
                return found;
            if ( ( found = Search( file.SupplementaryTokens, position ) ) != null )
                return found;

            return null;
        }

        #endregion Public Methods

        #region Private Methods

        private static DetailedContext Search( SortedList<int, Token> tokens, int position )
        {
            int index = tokens.LowerBound( position );
            if ( index >= 0 && index < tokens.Count && tokens.Values[ index ].Position.Start <= position && tokens.Values[ index ].Position.End >= position )
            {
                return tokens.Values[ index ].Declaration;
            }

            return null;
        }

        #endregion Private Methods

        #region Events

        public class FileUpdatedArgs : EventArgs
        {
            public FileUpdatedArgs( string file )
            {
                File = file;
            }

            public string File { get; private set; }
        }

        public class SnapshotUpdatedArgs : FileUpdatedArgs
        {
            public SnapshotUpdatedArgs( string file, ITextSnapshot snapshot ) : base( file )
            {
                Snapshot = snapshot;
            }

            public ITextSnapshot Snapshot { get; private set; }
        }

        public event EventHandler<SnapshotUpdatedArgs> Updated;

        #endregion Events
    }

    #endregion Database
}
